/*
 * Copyright (c) 2024 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseCustomDfuImpl } from './BaseCustomDfuImpl'
import { DfuBaseService } from './DfuBaseService'
import { SecureDfuError } from './error/SecureDfuError'
import ble from '@ohos.bluetooth.ble';
import fs from '@ohos.file.fs';
import hilog from '@ohos.hilog';
import { InputStream } from './internal/InputStream';
import { JSON } from '@kit.ArkTS';
import zlib from '@ohos.zlib';
import { ArchiveInputStream } from './internal/ArchiveInputStream';
import { CardRecognitionConfig } from '@kit.VisionKit';

class ObjectChecksum {
  offset: number;
  CRC32: number;
}

class ObjectInfo extends ObjectChecksum {
  maxSize: number;
}

export class SecureDfuImpl extends BaseCustomDfuImpl {
  // UUIDs used by the DFU
  static DEFAULT_DFU_SERVICE_UUID: string = '0000FE59-0000-1000-8000-00805F9B34FB'; // 16-bit UUID assigned by Bluetooth SIG
  static DEFAULT_DFU_CONTROL_POINT_UUID: string = '8EC90001-F315-4F60-9FB8-838830DAEA50';
  static DEFAULT_DFU_PACKET_UUID: string = '8EC90002-F315-4F60-9FB8-838830DAEA50';

  static DFU_SERVICE_UUID: string = SecureDfuImpl.DEFAULT_DFU_SERVICE_UUID;
  static DFU_CONTROL_POINT_UUID: string = SecureDfuImpl.DEFAULT_DFU_CONTROL_POINT_UUID;
  static DFU_PACKET_UUID: string = SecureDfuImpl.DEFAULT_DFU_PACKET_UUID;

  private static DFU_STATUS_SUCCESS: number = 1;
  private static MAX_ATTEMPTS: number = 3;

  // Object types
  private static OBJECT_COMMAND: number = 0x01;
  private static OBJECT_DATA: number = 0x02;
  // Operation codes and packets
  private static OP_CODE_CREATE_KEY: number = 0x01;
  private static OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY: number = 0x02;
  private static OP_CODE_CALCULATE_CHECKSUM_KEY: number = 0x03;
  private static OP_CODE_EXECUTE_KEY: number = 0x04;
  private static OP_CODE_SELECT_OBJECT_KEY: number = 0x06;
  private static OP_CODE_RESPONSE_CODE_KEY: number = 0x60;
  private static OP_CODE_CREATE_COMMAND: Uint8Array =
    new Uint8Array([SecureDfuImpl.OP_CODE_CREATE_KEY, SecureDfuImpl.OBJECT_COMMAND, 0x00, 0x00, 0x00, 0x00]);
  private static OP_CODE_CREATE_DATA: Uint8Array =
    new Uint8Array([SecureDfuImpl.OP_CODE_CREATE_KEY, SecureDfuImpl.OBJECT_DATA, 0x00, 0x00, 0x00, 0x00]);
  private static OP_CODE_PACKET_RECEIPT_NOTIF_REQ: Uint8Array =
    new Uint8Array([SecureDfuImpl.OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY, 0x00, 0x00 /* param PRN uint16 in Little Endian */]);
  private static OP_CODE_CALCULATE_CHECKSUM: Uint8Array =
    new Uint8Array([SecureDfuImpl.OP_CODE_CALCULATE_CHECKSUM_KEY]);
  private static OP_CODE_EXECUTE: Uint8Array =
    new Uint8Array([SecureDfuImpl.OP_CODE_EXECUTE_KEY]);
  private static OP_CODE_SELECT_OBJECT: Uint8Array =
    new Uint8Array([SecureDfuImpl.OP_CODE_SELECT_OBJECT_KEY, 0x00 /* type */]);

  private mControlPointCharacteristic: ble.BLECharacteristic;
  private mPacketCharacteristic: ble.BLECharacteristic;

  private prepareObjectDelay: number;
  private allowResume: boolean;

  constructor(service: DfuBaseService) {
    super(service);
    this.TAG = 'SecureDfuImpl';
  }

  public initialize(gatt: ble.GattClientDevice, fileType: number,
    firmwareStream: InputStream, initPacketStream: InputStream): boolean {
    if (initPacketStream == null) {
      hilog.error(this.DOMAIN, this.TAG, `initialize with null initPacketStream`);
      this.mService.terminateConnection(gatt, DfuBaseService.ERROR_INIT_PACKET_REQUIRED);
      return false;
    }

    return super.initialize(gatt, fileType, firmwareStream, initPacketStream);
  }

  protected getDfuServiceUUID(): string {
    return SecureDfuImpl.DFU_SERVICE_UUID;
  }

  protected getControlPointCharacteristicUUID(): string {
    return SecureDfuImpl.DFU_CONTROL_POINT_UUID;
  }

  protected getPacketCharacteristicUUID(): string {
    return SecureDfuImpl.DFU_PACKET_UUID;
  }

  public async isClientCompatible(gattServiceList: Array<ble.GattService>): Promise<boolean> {
    hilog.info(this.DOMAIN, this.TAG, 'SecureDfuImpl isClientCompatible');
    //TODO: find service with spec UUID
    for (const gsItem of gattServiceList) {
      if (gsItem.serviceUuid === SecureDfuImpl.DFU_SERVICE_UUID) {
        for (const gcItem of gsItem.characteristics) {
          if (gcItem.characteristicUuid === SecureDfuImpl.DFU_CONTROL_POINT_UUID) {
            for (const gdItem of gcItem.descriptors) {
              if (gdItem.descriptorUuid === SecureDfuImpl.CLIENT_CHARACTERISTIC_CONFIG.toUpperCase()) {
                this.mControlPointCharacteristic = gcItem;
                hilog.info(this.DOMAIN, this.TAG, 'Is SecureDfuImpl Client');
                for (const gcItem2 of gsItem.characteristics) {
                  if (gcItem2.characteristicUuid === SecureDfuImpl.DFU_PACKET_UUID) {
                    this.mPacketCharacteristic = gcItem2;
                    return true;
                  }
                }
              }
            }
          }
        }
      }
    }
    return false;
  }

  public shouldScanForBootloader(): boolean {
    return false;
  };

  public finalize(forceRefresh: boolean) {
    return;
  };

  public async requestMtu(mtu: number): Promise<void> {
    let that = this;
    let presolve: ((value: void | PromiseLike<void>) => void) | null = null;
    this.mGatt.on("BLEMtuChange", (mtures: number) => {
      hilog.info(this.DOMAIN, this.TAG, 'BLEMtuChange, mtu: ' + mtures);
      that.mBuffer = new Uint8Array(mtures-3);
      hilog.info(this.DOMAIN, this.TAG, "MTU changed to: " + mtures);
      this.mGatt.off("BLEMtuChange");
      presolve();
    })
    return new Promise<void>((resolve) => {
      try {
        presolve = resolve;
        this.mGatt.setBLEMtuSize(mtu);
      } catch (e) {
        hilog.error(this.DOMAIN, this.TAG, `requestMtu err: ${JSON.stringify(e)}`);
      }
    })
  }

  private unsignedBytesToInt(array: Uint8Array, offset: number): number {
    return (array[offset] & 0xFF) + ((array[offset + 1] & 0xFF) << 8)
      + ((array[offset + 2] & 0xFF) << 16) + ((array[offset + 3] & 0xFF) << 24);
  }

  private getStatusCode(response: Uint8Array, request: number): number {
    if (response == null || response.length < 3 || response[0] != SecureDfuImpl.OP_CODE_RESPONSE_CODE_KEY ||
      response[1] != request || (response[2] != SecureDfuImpl.DFU_STATUS_SUCCESS &&
      response[2] != SecureDfuError.OP_CODE_NOT_SUPPORTED &&
      response[2] != SecureDfuError.INVALID_PARAM &&
      response[2] != SecureDfuError.INSUFFICIENT_RESOURCES &&
      response[2] != SecureDfuError.INVALID_OBJECT &&
      response[2] != SecureDfuError.UNSUPPORTED_TYPE &&
      response[2] != SecureDfuError.OPERATION_NOT_PERMITTED &&
      response[2] != SecureDfuError.OPERATION_FAILED &&
      response[2] != SecureDfuError.EXTENDED_ERROR)) {
      throw new Error(`Invalid response received ${JSON.stringify(response)},` +
        `request[${request}], ${SecureDfuImpl.OP_CODE_RESPONSE_CODE_KEY}`);
      // hilog.error(this.DOMAIN, this.TAG, `Invalid response received ${JSON.stringify(response)},` +
      //    `${SecureDfuImpl.OP_CODE_RESPONSE_CODE_KEY}`)
    }
    return response[2];
  }

  private async selectObject(type: number): Promise<ObjectInfo> {
    if (!this.mConnected) {
      throw new Error("Unable to read object info: device disconnected");
    }
    let presolve: ((value: ObjectInfo | PromiseLike<ObjectInfo>) => void) | null = null;
    let that = this;
    this.mGatt.on("BLECharacteristicChange", (ccRes: ble.BLECharacteristic) => {
      hilog.info(that.DOMAIN, that.TAG, `selectobject BLECharacteristicChange: ${JSON.stringify(ccRes)}`);

      //TODO: the following should get value from
      let response: Uint8Array = new Uint8Array(ccRes.characteristicValue);
      let status: number = that.getStatusCode(response, SecureDfuImpl.OP_CODE_SELECT_OBJECT_KEY);
      if (status == SecureDfuError.EXTENDED_ERROR) {
        hilog.info(that.DOMAIN, that.TAG, "Selecting object failed" + status);
        throw new Error("Selecting object failed" + response[3]);
      }
      if (status != SecureDfuImpl.DFU_STATUS_SUCCESS) {
        hilog.info(that.DOMAIN, that.TAG, "Selecting object failed" + status);
        throw new Error("Selecting object failed" + status);
      }

      let info: ObjectInfo = new ObjectInfo();
      info.maxSize = that.unsignedBytesToInt(response, 3);
      info.offset = that.unsignedBytesToInt(response, 3 + 4);
      info.CRC32  = that.unsignedBytesToInt(response, 3 + 8);
      that.mGatt.off("BLECharacteristicChange");
      presolve(info);
    })
    SecureDfuImpl.OP_CODE_SELECT_OBJECT[1] = type;
    return new Promise<ObjectInfo>((resovle) => {
      presolve = resovle;
      this.writeOpCode(this.mControlPointCharacteristic, SecureDfuImpl.OP_CODE_SELECT_OBJECT, false);
    })
  }

  private setNumberOfPackets(data: Uint8Array, value: number) {
    data[1] = (value & 0xFF);
    data[2] = ((value >> 8) & 0xFF);
  }

  private async setPacketReceiptNotifications(value: number): Promise<void> {
    if (!this.mConnected) {
      hilog.error(this.DOMAIN, this.TAG, "Unable to read Checksum: device disconnected")
      throw new Error("Unable to read Checksum: device disconnected");
    }

    // Send the number of packets of firmware before receiving a receipt notification
    hilog.info(this.DOMAIN, this.TAG,
      "Sending the number of packets before notifications (Op Code = 2, Value = " + value + ")");

    let presolve: ((value: void | PromiseLike<void>) => void) | null = null;
    let that = this;
    this.mGatt.on("BLECharacteristicChange", (ccRes: ble.BLECharacteristic) => {
      hilog.info(that.DOMAIN, that.TAG, `setPacketReceipt BLECharacteristicChange: ${JSON.stringify(ccRes)}`);

      //TODO: the following should get value from
      // Read response
      let response: Uint8Array = new Uint8Array(ccRes.characteristicValue);

      let status: number = this.getStatusCode(response, SecureDfuImpl.OP_CODE_PACKET_RECEIPT_NOTIF_REQ_KEY);
      if (status == SecureDfuError.EXTENDED_ERROR) {
        hilog.error(that.DOMAIN, that.TAG, `Sending the number of packets failed: ${response[3]}`);
        throw new Error("Sending the number of packets failed: " + response[3]);
      }

      if (status != SecureDfuImpl.DFU_STATUS_SUCCESS) {
        hilog.error(that.DOMAIN, that.TAG, `Sending the number of packets failed: ${status}`);
        throw new Error("Sending the number of packets failed: " + status);
      }
      this.mGatt.off("BLECharacteristicChange");
      presolve();
    })
    return new Promise<void>((resolve) => {
      presolve = resolve;
      this.setNumberOfPackets(SecureDfuImpl.OP_CODE_PACKET_RECEIPT_NOTIF_REQ, value);
      this.writeOpCode(this.mControlPointCharacteristic, SecureDfuImpl.OP_CODE_PACKET_RECEIPT_NOTIF_REQ, false);
    })
  }

  private setObjectSize(data: Uint8Array, value: number) {
    data[2] = (value & 0xFF);
    data[3] = ((value >> 8) & 0xFF);
    data[4] = ((value >> 16) & 0xFF);
    data[5] = ((value >> 24) & 0xFF);
  }

  private async writeCreateRequest(type: number, size: number): Promise<void> {
    if (!this.mConnected) {
      throw new Error("Unable to create object: device disconnected");
    }

    let presolve: ((value: void | PromiseLike<void>) => void) | null = null;
    let that = this;
    this.mGatt.on("BLECharacteristicChange", (ccRes: ble.BLECharacteristic) => {
      hilog.info(that.DOMAIN, that.TAG, `writeCreateRequest BLECharacteristicChange: ${JSON.stringify(ccRes)}`);

      //TODO: the following should get value from
      // Read response
      let response: Uint8Array = new Uint8Array(ccRes.characteristicValue);

      let status: number = this.getStatusCode(response, SecureDfuImpl.OP_CODE_CREATE_KEY);
      if (status == SecureDfuError.EXTENDED_ERROR) {
        hilog.error(this.DOMAIN, this.TAG, "Creating Command object failed" + response[3]);
        throw new Error("Creating Command object failed" + response[3]);
      }
      if (status != SecureDfuImpl.DFU_STATUS_SUCCESS) {
        hilog.error(this.DOMAIN, this.TAG, "Creating Command object failed" + status);
        throw new Error("Creating Command object failed" + status);
      }
      this.mGatt.off("BLECharacteristicChange");
      presolve();
    })

    return new Promise<void>(async (resolve) => {
      presolve = resolve;
      let data: Uint8Array = (type == SecureDfuImpl.OBJECT_COMMAND) ?
        SecureDfuImpl.OP_CODE_CREATE_COMMAND : SecureDfuImpl.OP_CODE_CREATE_DATA;
      this.setObjectSize(data, size);
      await this.writeOpCode(this.mControlPointCharacteristic, data, false);
    })
  }

  private async readChecksum(): Promise<ObjectChecksum> {
    if (!this.mConnected) {
      throw new Error("Unable to read Checksum: device disconnected");
    }

    let presolve: ((value: ObjectChecksum | PromiseLike<ObjectChecksum>) => void) | null = null;
    let that = this;
    this.mGatt.on("BLECharacteristicChange", (characteristic: ble.BLECharacteristic) => {
      hilog.info(this.DOMAIN, this.TAG, 'readChecksum BLECharacteristicChange');

      let response: Uint8Array = new Uint8Array(characteristic.characteristicValue);
      let status: number = that.getStatusCode(response, SecureDfuImpl.OP_CODE_CALCULATE_CHECKSUM_KEY);
      if (status == SecureDfuError.EXTENDED_ERROR) {
        throw new Error("Receiving Checksum failed" + response[3]);
      }
      if (status != SecureDfuImpl.DFU_STATUS_SUCCESS) {
        throw new Error("Receiving Checksum failed" + status);
      }

      let checksum: ObjectChecksum = new ObjectChecksum();
      checksum.offset = that.unsignedBytesToInt(response, 3);
      checksum.CRC32  = that.unsignedBytesToInt(response, 3 + 4);
      this.mGatt.off("BLECharacteristicChange");
      presolve(checksum);
    })

    return new Promise<ObjectChecksum>((resolve) => {
      presolve = resolve;
      this.writeOpCode(this.mControlPointCharacteristic, SecureDfuImpl.OP_CODE_CALCULATE_CHECKSUM, false);
    })
  }

  private async writeExecuteRetry(allowRetry: boolean) {
    return this.writeExecute();
  }

  private async writeExecute(): Promise<void> {
    if (!this.mConnected) {
      throw new Error("Unable to read Checksum: device disconnected");
    }

    //TODO：问题来了，这个onWrite的回调没有，所以等不出来，卡卡，要疯
    await this.writeOpCode(this.mControlPointCharacteristic, SecureDfuImpl.OP_CODE_EXECUTE, false);
    await this.onCharacteristicWrite(this.mControlPointCharacteristic);
  }

  public async sendInitPacket(gatt: ble.GattClientDevice, allowResume: boolean) {
    let checksumObj: ObjectChecksum;
    let checksum: zlib.Checksum = zlib.createChecksumSync();
    hilog.info(this.DOMAIN, this.TAG, "Setting object to Command (Op Code = 6, Type = 1)");
    let oinfo: ObjectInfo = await this.selectObject(SecureDfuImpl.OBJECT_COMMAND);
    hilog.info(this.DOMAIN, this.TAG, `Command object info received ${JSON.stringify(oinfo)}`);
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
      `Command object info received ${JSON.stringify(oinfo)}`);

    //noinspection StatementWithEmptyBody
    if (this.mInitPacketSizeInBytes > oinfo.maxSize) {
      // Ignore this here. Later, after sending the 'Create object' command, DFU target will send an error if init packet is too large
      hilog.warn(this.DOMAIN, this.TAG, `this[${this.mInitPacketSizeInBytes > oinfo.maxSize}] > info[${oinfo.maxSize}]`);
    }

    let skipSendingInitPacket: boolean = false;
    let resumeSendingInitPacket: boolean = false;
    let crc: number = 0;
    if (this.allowResume && oinfo.offset > 0 && oinfo.offset <= this.mInitPacketSizeInBytes) {
      try {
        // Read the same number of bytes from the current init packet to calculate local CRC32
        let rbuffer: Uint8Array = new Uint8Array[oinfo.offset];
        //noinspection ResultOfMethodCallIgnored
        rbuffer =  this.mInitPacketStream.read(0, rbuffer.length);
        // Calculate the CRC32
        // crc32.update(buffer);
        crc = await checksum.crc32(0, rbuffer.buffer);
        // let crc: number = (crc32.getValue() & 0xFFFFFFFF);
        hilog.info(this.DOMAIN, this.TAG, `oinfo.CRC32[${oinfo.CRC32}], crc[${crc}]`);
        if (oinfo.CRC32 == crc) {
          hilog.info(this.DOMAIN, this.TAG, "Init packet CRC is the same");
          if (oinfo.offset ==  this.mInitPacketSizeInBytes) {
            // The whole init packet was sent and it is equal to one we try to send now.
            // There is no need to send it again. We may try to resume sending data.
            hilog.info(this.DOMAIN, this.TAG, "-> Whole Init packet was sent before");
            skipSendingInitPacket = true;
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Received CRC match Init packet");
          } else {
            hilog.info(this.DOMAIN, this.TAG, "-> " + oinfo.offset + " bytes of Init packet were sent before");
            resumeSendingInitPacket = true;
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Resuming sending Init packet...");
          }
        } else {
          // A different Init packet was sent before, or the error occurred while sending.
          // We have to send the whole Init packet again.
          this.mInitPacketStream.reset();
          // crc32.reset();
          oinfo.offset = 0;
        }
      } catch (e) {
        hilog.error(this.DOMAIN, this.TAG, "Error while reading " +
          oinfo.offset + " bytes from the init packet stream", e);
        try {
          // Go back to the beginning of the stream, we will send the whole init packet
          this.mInitPacketStream.reset();
          // crc32.reset();
          oinfo.offset = 0;
        } catch (e1) {
          hilog.error(this.DOMAIN, this.TAG, "Error while resetting the init packet stream", e1);
          this.mService.terminateConnection(this.mGatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
          return;
        }
      }
    }

    if (!skipSendingInitPacket) {
      // The Init packet is sent different way in this implementation than the firmware, and receiving PRNs is not implemented.
      // This value might have been stored on the device, so we have to explicitly disable PRNs.
      await this.setPacketReceiptNotifications(0);
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
        "Packet Receipt Notif disabled (Op Code = 2, Value = 0)");

      for (let attempt = 1; attempt <= SecureDfuImpl.MAX_ATTEMPTS;) {
        if (!resumeSendingInitPacket) {
          // Create the Init object
          hilog.info(this.DOMAIN, this.TAG, "Creating Init packet object (Op Code = 1, Type = 1, Size = " +
            this.mInitPacketSizeInBytes + ")");
          await this.writeCreateRequest(SecureDfuImpl.OBJECT_COMMAND, this.mInitPacketSizeInBytes);
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Command object created");
        }
        // Write Init data to the Packet Characteristic
        try {
          hilog.info(this.DOMAIN, this.TAG, "Sending " +
            (this.mInitPacketSizeInBytes - oinfo.offset) + " bytes of init packet...");
          await this.writeInitData(this.mPacketCharacteristic, checksum);
        } catch (e) {
          hilog.info(this.DOMAIN, this.TAG, `Disconnected while sending init packet: ${JSON.stringify(e)}`);
          throw e;
        }
        crc = (this.updateCrc & 0xFFFFFFFF);
        this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, `Command object sent (CRC = ${crc})`);

        // Calculate Checksum
        hilog.info(this.DOMAIN, this.TAG, "writeInitData Sending Calculate Checksum command (Op Code = 3)");
        checksumObj = await this.readChecksum();
        this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
          `Checksum received (Offset = ${checksumObj.offset}, CRC = ${checksumObj.CRC32})`);
        hilog.info(this.DOMAIN, this.TAG,
          `Checksum received (Offset=${checksumObj.offset}, CRC32=${checksumObj.CRC32})), crc=${crc}`);

        if (crc == checksumObj.CRC32) {
          // Everything is OK, we can proceed
          hilog.info(this.DOMAIN, this.TAG, 'Everything is OK, we can proceed');
          break;
        } else {
          hilog.error(this.DOMAIN, this.TAG, `CRC[${crc}] not match obj.CRC32[${checksumObj.CRC32}]`);
          if (attempt < SecureDfuImpl.MAX_ATTEMPTS) {
            attempt++;
            hilog.info(this.DOMAIN, this.TAG, "CRC does not match! Retrying...(" + attempt + "/" + SecureDfuImpl.MAX_ATTEMPTS + ")");
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
              "CRC does not match! Retrying...(" + attempt + "/" + SecureDfuImpl.MAX_ATTEMPTS + ")");
            try {
              // Go back to the beginning, we will send the whole Init packet again
              resumeSendingInitPacket = false;
              oinfo.offset = 0;
              oinfo.CRC32 = 0;
              this.mInitPacketStream.reset();
              // crc32.reset();
            } catch (e) {
              hilog.error(this.DOMAIN, this.TAG, "Error while resetting the init packet stream", e);
              this.mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
              return;
            }
          } else {
            hilog.error(this.DOMAIN, this.TAG, "CRC does not match!");
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, "CRC does not match!");
            this.mService.terminateConnection(gatt, DfuBaseService.ERROR_CRC_ERROR);
            return;
          }
        }
      }
    }

    // Execute Init packet. It's better to execute it twice than not execute at all...
    hilog.info(this.DOMAIN, this.TAG, "Executing init packet (Op Code = 4)");
    await this.writeExecute();
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Command object executed");
  }

  private async sendFirmware(gatt: ble.GattClientDevice) {
    if (this.mAborted) {
      hilog.warn(this.DOMAIN, this.TAG, "Upload aborted");
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Upload aborted");
      this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_ABORTED);
      this.mService.terminateConnection(gatt, DfuBaseService.ERROR_DEVICE_DISCONNECTED);
    }
    // Send the number of packets of firmware before receiving a receipt notification
    let numberOfPacketsBeforeNotification: number = this.mPacketsBeforeNotification;
    if (numberOfPacketsBeforeNotification > 0) {
      this.setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
        "Packet Receipt Notif Req (Op Code = 2) sent (Value = " + numberOfPacketsBeforeNotification + ")");
    }

    // We are ready to start sending the new firmware.

    hilog.info(this.DOMAIN, this.TAG, "Setting object to Data (Op Code = 6, Type = 2)");
    let info: ObjectInfo = await this.selectObject(SecureDfuImpl.OBJECT_DATA);
    hilog.info(this.DOMAIN, this.TAG, "Data object info received (Max size= %d, Offset = %d, CRC = %d)",
      info.maxSize, info.offset, info.CRC32);
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
      `Data object info received (Max size=${info.maxSize}, Offset=${info.offset}, CRC=${info.CRC32})`);
    this.mProgressInfo.setMaxObjectSizeInBytes(info.maxSize);

    // Number of chunks in which the data will be sent
    let chunkCount: number = (this.mImageSizeInBytes + info.maxSize - 1) / info.maxSize;
    let currentChunk: number = 0;
    let resumeSendingData: boolean = false;

    // Can we resume? If the offset obtained from the device is greater then zero we can compare it with the local CRC
    // and resume sending the data.
    //TODO: resume workflow, usually unreached
    if (info.offset > 0) {
      try {
        currentChunk = info.offset / info.maxSize;
        let bytesSentAndExecuted: number = info.maxSize * currentChunk;
        let bytesSentNotExecuted: number = info.offset - bytesSentAndExecuted;

        // If the offset is dividable by maxSize, assume that the last page was not executed
        if (bytesSentNotExecuted == 0) {
          bytesSentAndExecuted -= info.maxSize;
          bytesSentNotExecuted = info.maxSize;
        }

        // Read the same number of bytes from the current init packet to calculate local CRC32
        if (bytesSentAndExecuted > 0) {
          //noinspection ResultOfMethodCallIgnored
          let bsBuf = new Uint8Array(bytesSentAndExecuted)
          let [rsize, rebuf] = await (this.mFirmwareStream as ArchiveInputStream).readByBuf(bsBuf); // Read executed bytes
          this.mFirmwareStream.mark(info.maxSize); // Mark here
        }
        // Here the bytesSentNotExecuted is for sure greater then 0
        //noinspection ResultOfMethodCallIgnored
        let restBuf: Uint8Array = new Uint8Array(bytesSentNotExecuted)
        let [size, rebuf] = await (this.mFirmwareStream as ArchiveInputStream).readByBuf(restBuf); // Read the rest

        // Calculate the CRC32
        let archStream: ArchiveInputStream = this.mFirmwareStream as ArchiveInputStream;
        let crc: number = await archStream.getCrc32();

        if (crc == info.CRC32) {
          hilog.info(this.DOMAIN, this.TAG, info.offset + " bytes of data sent before, CRC match");
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
            info.offset + " bytes of data sent before, CRC match");
          this.mProgressInfo.setBytesSent(info.offset);
          this.mProgressInfo.setBytesReceived(info.offset);

          // If the whole page was sent and CRC match, we have to make sure it was executed
          if (bytesSentNotExecuted == info.maxSize && info.offset < this.mImageSizeInBytes) {
            hilog.info(this.DOMAIN, this.TAG, "Executing data object (Op Code = 4)");
            try {
              await this.writeExecute();
              this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed");
            } catch (e) {
              // In DFU bootloader from SDK 15.x, 16 and 17 there's a bug, which
              // prevents executing an object that has already been executed.
              // See: https://github.com/NordicSemiconductor/Android-DFU-Library/issues/252
              if (e.getErrorNumber() != SecureDfuError.OPERATION_NOT_PERMITTED) {
                throw e;
              }
              this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object already executed");
              // At this point, the error flag should be cleared, and the subsequent process can be executed normally.
              this.mRemoteErrorOccurred = false;
            }
          } else {
            resumeSendingData = true;
          }
        } else {
          hilog.info(this.DOMAIN, this.TAG, info.offset + " bytes sent before, CRC does not match");
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
            info.offset + " bytes sent before, CRC does not match");
          // The CRC of the current object is not correct. If there was another Data object sent before, its CRC must have been correct,
          // as it has been executed. Either way, we have to create the current object again.
          this.mProgressInfo.setBytesSent(bytesSentAndExecuted);
          this.mProgressInfo.setBytesReceived(bytesSentAndExecuted);
          info.offset -= bytesSentNotExecuted;
          info.CRC32 = 0; // invalidate
          this.mFirmwareStream.reset();
          hilog.info(this.DOMAIN, this.TAG, "Resuming from byte " + info.offset + "...");
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
            "Resuming from byte " + info.offset + "...");
        }
      } catch (e) {
        hilog.error(this.DOMAIN, this.TAG, "Error while reading firmware stream", e);
        this.mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
        return;
      }
    } else {
      // Initialize the timer used to calculate the transfer speed
      this.mProgressInfo.setBytesSent(0);
    }

    let startTime: number = Date.now();

    if (info.offset < this.mImageSizeInBytes) {
      let attempt: number = 1;
      // Each page will be sent in MAX_ATTEMPTS
      while (this.mProgressInfo.getAvailableObjectSizeIsBytes() > 0) {
        if (!resumeSendingData) {
          // Create the Data object
          let availableObjectSizeInBytes: number = this.mProgressInfo.getAvailableObjectSizeIsBytes();
          hilog.info(this.DOMAIN, this.TAG, "Creating Data object (Op Code = 1, Type = 2, Size = " +
            availableObjectSizeInBytes + ") (" + (currentChunk + 1) + "/" + chunkCount + ")");
          await this.writeCreateRequest(SecureDfuImpl.OBJECT_DATA, availableObjectSizeInBytes);
          await this.mService.waitFor(400);
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
            "Data object (" + (currentChunk + 1) + "/" + chunkCount + ") created");
          // Waiting until the device is ready to receive the data object.
          // If prepare data object delay was set in the initiator, the delay will be used
          // for all data objects.
          if (this.prepareObjectDelay > 0 || chunkCount == 0) {
            // TODO: need write a sleep function
            await this.mService.waitFor(this.prepareObjectDelay > 0 ? this.prepareObjectDelay : 400);
          }
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
            "Uploading firmware...");
        } else {
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
            "Resuming uploading firmware...");
          resumeSendingData = false;
        }

        // Send the current object part
        try {
          hilog.info(this.DOMAIN, this.TAG, "Uploading firmware...");
          await this.uploadFirmwareImage(this.mPacketCharacteristic);
        } catch (e) {
          hilog.error(this.DOMAIN, this.TAG, `Disconnected while sending data ${JSON.stringify(e)}`);
          throw e;
        }

        // Calculate Checksum
        hilog.info(this.DOMAIN, this.TAG, "uploadFirmwareImage Sending Calculate Checksum command (Op Code = 3)");
        let checksum: ObjectChecksum = await this.readChecksum();
        hilog.info(this.DOMAIN, this.TAG, `Checksum received (Offset = ${checksum.offset}, CRC = ${checksum.CRC32})`);
        this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, `Checksum received (Offset=${checksum.offset}, CRC=${checksum.CRC32})`);

        // It may happen, that not all bytes that were sent were received by the remote device
        let bytesLost: number = this.mProgressInfo.getBytesSent() - checksum.offset;
        if (bytesLost > 0) {
          hilog.info(this.DOMAIN, this.TAG, bytesLost + " bytes were lost!");
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
            bytesLost + " bytes were lost");

          try {
            // We have to reset the stream and read 'offset' number of bytes to recalculate the CRC
            this.mFirmwareStream.reset(); // Resets to the beginning of current object
            //noinspection ResultOfMethodCallIgnored
            let buf: Uint8Array = new Uint8Array(info.maxSize - bytesLost)
            let [readSize, rebuf] = await (this.mFirmwareStream as ArchiveInputStream).readByBuf(buf); // Reads additional bytes that were sent and received in this object
            this.mProgressInfo.setBytesSent(checksum.offset);
          } catch (e) {
            hilog.error(this.DOMAIN, this.TAG, "Error while reading firmware stream", e);
            this.mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
            return;
          }
          // To decrease the chance of loosing data next time let's set PRN to 1.
          // This will make the update very long, but perhaps it will succeed.
          let newPrn: number = 1;
          if (this.mPacketsBeforeNotification == 0 || this.mPacketsBeforeNotification > newPrn) {
            numberOfPacketsBeforeNotification = this.mPacketsBeforeNotification = newPrn;
            this.setPacketReceiptNotifications(numberOfPacketsBeforeNotification);
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
              "Packet Receipt Notif Req (Op Code = 2) sent (Value = " + newPrn + ")");
          }
        }

        //TODO: see you next week
        // Calculate the CRC32
        let crc: number = (await (this.mFirmwareStream as ArchiveInputStream).getCrc32()) & 0xFFFFFFFF;
        if (crc == checksum.CRC32) {
          if (bytesLost > 0) {
            resumeSendingData = true;
            continue;
          }
          // Execute Init packet
          hilog.info(this.DOMAIN, this.TAG, "Executing data object (Op Code = 4)");
          await this.writeExecuteRetry(this.mProgressInfo.isComplete());
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed");

          // Increment iterator
          currentChunk++;
          attempt = 1;
          //Mark this location after completion of successful transfer.  In the event of a CRC retry on the next packet we will restart from this point.
          this.mFirmwareStream.mark(0);
        } else {
          let crcFailMessage: string = `CRC does not match! Expected ${crc} but found ${checksum.CRC32}.`;
          if (attempt < SecureDfuImpl.MAX_ATTEMPTS) {
            attempt++;
            crcFailMessage += ` Retrying...(${attempt}/${SecureDfuImpl.MAX_ATTEMPTS})`;
            hilog.info(this.DOMAIN, this.TAG, crcFailMessage);
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, crcFailMessage);
            try {
              // Reset the CRC and file pointer back to the previous mark() point after completion of the last successful packet.
              this.mFirmwareStream.reset();
              this.mProgressInfo.setBytesSent((this.mFirmwareStream as ArchiveInputStream).getBytesRead());
            } catch (e) {
              hilog.error(this.DOMAIN, this.TAG, "Error while resetting the firmware stream", e);
              this.mService.terminateConnection(gatt, DfuBaseService.ERROR_FILE_IO_EXCEPTION);
              return;
            }
          } else {
            hilog.error(this.DOMAIN, this.TAG, crcFailMessage);
            this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_ERROR, crcFailMessage);
            this.mService.terminateConnection(gatt, DfuBaseService.ERROR_CRC_ERROR);
            return;
          }
        }
      }
    } else {
      // Looks as if the whole file was sent correctly but has not been executed
      hilog.info(this.DOMAIN, this.TAG, "Executing data object (Op Code = 4)");
      // await this.writeExecute(true);
      await this.writeExecute();
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Data object executed");
    }

    let endTime: number = Date.now();
    hilog.info(this.DOMAIN, this.TAG, "Transfer of " + (this.mProgressInfo.getBytesSent() - info.offset) +
      " bytes has taken " + (endTime - startTime) + " ms");
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Upload completed in " + (endTime - startTime) + " ms");
  }

  public async performDfu() {
    hilog.info(this.DOMAIN, this.TAG, 'SecureDfuImpl performDfu');
    this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_STARTING);

    let gatt: ble.GattClientDevice = this.mGatt;

    // Let's request the MTU requested by the user. It may be that a lower MTU will be used.
    let requiredMtu: number = 517;
    hilog.info(this.DOMAIN, this.TAG, `Requesting MTU = ${requiredMtu}`);
    await this.requestMtu(requiredMtu);

    this.prepareObjectDelay = 0;

    try {
      // Enable notification
      await this.enableCCCD(this.mControlPointCharacteristic, SecureDfuImpl.NOTIFICATIONS);
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION,
        "Notifications enabled");
      hilog.info(this.DOMAIN, this.TAG, 'Notifications enabled');
      try {
        //TODO: default allowResume is true, optional canbe set by user
        let allowResume: boolean = true;
        await this.sendInitPacket(gatt, allowResume);
      } catch (e) {
        hilog.error(this.DOMAIN, this.TAG, `sendInitPacket: ${JSON.stringify(e)}`);
        if (!this.mProgressInfo.isLastPart()) {
          this.mRemoteErrorOccurred = false;

          hilog.warn(this.DOMAIN, this.TAG, "Sending SD+BL failed. Trying to send App only");
          this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING,
            "Invalid system components. Trying to send application");
          this.mFileType = DfuBaseService.TYPE_APPLICATION;

          // Set new content type in the ZIP Input Stream and update sizes of images
          let zhis: ArchiveInputStream = this.mFirmwareStream as ArchiveInputStream;
          zhis.setContentType(this.mFileType);
          let applicationInit: Uint8Array = zhis.getApplicationInit();
          this.mInitPacketStream = new InputStream(applicationInit);
          this.mInitPacketSizeInBytes = applicationInit.length;
          this.mImageSizeInBytes = zhis.applicationImageSize();
          this.mProgressInfo.init(this.mImageSizeInBytes, 2, 2);

          await this.sendInitPacket(gatt, false);
        } else {
          // There's noting we could do about it.
          hilog.error(this.DOMAIN, this.TAG, `this.mProgressInfo.isLastPart`)
          throw e;
        }
      }

      await this.sendFirmware(gatt);

      //TODO: see you next week;
      // The device will reset so we don't have to send Disconnect signal.
      this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_DISCONNECTING);
      this.mService.waitUntilDisconnected(this.mGatt);
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "Disconnected by the remote device");

      // We are ready with DFU, the device is disconnected, let's close it and finalize the operation.
      this.finalize(false);
    } catch (e) {
      hilog.error(this.DOMAIN, this.TAG, `performDfu err: ${JSON.stringify(e)}`);
      let error: number = DfuBaseService.ERROR_INVALID_RESPONSE;
      this.mService.terminateConnection(this.mGatt, error);
    }


  }
}